/*
 * Created on 2004-01-07
 */
package org.opencube.oms;

import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.rmi.server.UID;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;

import org.opencube.oms.meta.ComplexAttribute;
import org.opencube.oms.meta.OMSMetaDataException;
import org.opencube.util.Constants;

/**
 * @author <a href="mailto:maciek@fingo.pl">FINGO - Maciej Mroczko </a> TODO:
 *         comment
 */
public class OMSStructure implements Serializable {
	/* merge constants */
	public static final int MERGE_MODE_REFRESH = 2;

	public static final int MERGE_MODE_ADD = 1;

	public static final int MERGE_MODE_REPLACE = 0;

	public static final int MERGE_MODE_REMOVE = -1;

	/* The name of the structure root element */
	private static final String ROOT_ELEMENT_NAME = "VirtualRoot";

	/* The name of the structure trash element */
	private static final String TRASH_ELEMENT_NAME = "Trash";

	/* The contaner scheme namespace name */
	static final String CONTAINER_NAMESPACE_PATH = "/omscube";

	/* The container scheme name */
	static final String CONTAINER_SCHEME = "Container";

	/* The object attribute */
	static final String OBJECTS_ATTRIBUTE = "objects";

	/* The structure root */
	private org.opencube.oms.OMSElement rootElement = null;

	/* All elements in this structure by ids */
	private HashMap elementsById = null;

	/* All elements in this structure by bookmarks */
	private HashMap elementsByBookmark = null;

	/* Compositions: { element->parent } map */
	private HashMap compositions = null;

	/* The removed relations */
	private LinkedList relationTrash = new LinkedList();

	/* The removed elements */
	private LinkedList elementTrash = new LinkedList();

	/**
	 * @param metaData
	 * @param elements
	 * @param relations
	 * @param userId
	 */
	public OMSStructure() {
		this(10);
	}

	public OMSStructure(int capacity) {
		this.rootElement = new org.opencube.oms.OMSElement(this, CONTAINER_NAMESPACE_PATH, CONTAINER_SCHEME, Constants.STR_ZERO, ROOT_ELEMENT_NAME, ROOT_ELEMENT_NAME, new Date(), Constants.STR_ZERO,
				new Date(), Constants.STR_ZERO);
		this.elementsById = new HashMap(capacity);
		this.elementsByBookmark = new HashMap(capacity);
		this.compositions = new HashMap(capacity);
	}

	/**
	 * Returns rootElement.
	 * 
	 * @return Element - the root of this structure
	 */
	public org.opencube.oms.OMSElement getRootElement() {
		return this.rootElement;
	}

	/**
	 * Returns all elements in this structure
	 * 
	 * @return Element[] - all elements in this structure
	 */
	public org.opencube.oms.OMSElement[] getElements() {
		return (org.opencube.oms.OMSElement[]) this.elementsById.values().toArray(new org.opencube.oms.OMSElement[this.elementsById.size()]);
	}

	/**
	 * Creates new element under root element and appends it to this structure
	 * 
	 * @param scheme -
	 *            the scheme for the element
	 * 
	 * @return Element - newly created element
	 * 
	 * @throws OMSMetaDataException
	 */
	public org.opencube.oms.OMSElement createElement(String namespacePath, String scheme) {
		return createAndAppendNewElement(namespacePath, scheme, this.rootElement);
	}

	/**
	 * Creates new element with the given scheme, composes it under the the
	 * 2given element attribute and append to this structure
	 * 
	 * @param scheme -
	 *            the scheme
	 * @param parent -
	 *            the element parent
	 * @param attribute -
	 *            attribute to compose
	 * 
	 * @return Element - the newly created element
	 * 
	 * @throws OMSMetaDataException -
	 *             when the attribute is not found in the parent element scheme
	 *             hierarchy, or the attribute is not a composition
	 */
	org.opencube.oms.OMSElement createAndAppendNewElement(String namespacePath, String schemeName, org.opencube.oms.OMSElement parent) {
		String newId = getNewId();
		String newBookmark = getNewBookmark();
		org.opencube.oms.OMSElement element = createAndAppendElement(parent, namespacePath, schemeName, newId, null, newBookmark, new Date(), null, new Date(), null);
		element.setState(org.opencube.oms.OMSNode.STATE_NEW);

		return element;
	}

	/**
	 * Returns element with the given id
	 * 
	 * @param id
	 *            the id of the element to find
	 * 
	 * @return The element with the given id
	 */
	public org.opencube.oms.OMSElement getElementById(String id) {
		return (org.opencube.oms.OMSElement) this.elementsById.get(id);
	}

	/**
	 * Returns element with the given bookmark
	 * 
	 * @param bookmark
	 *            the bookmark of the element to find
	 * 
	 * @return The element with the given bookmark or null if not found
	 */
	public org.opencube.oms.OMSElement getElementByBookmark(String bookmark) {
		return (org.opencube.oms.OMSElement) this.elementsByBookmark.get(bookmark);
	}

	/* Returns the unique id */
	String getNewId() {
		return generateUniqueString();
	}

	/* Returns the unique bookmark */
	String getNewBookmark() {
		return generateUniqueString();
	}

	/* Returns the unique key for relations */
	String getNewKey() {
		return generateUniqueString();
	}

	/**
	 * Generate unique string
	 * 
	 * @return String new bookmark
	 */
	static String generateUniqueString() {
		UID uid = new UID();
		String ip = "";
		try {
			ip = InetAddress.getLocalHost().getHostAddress();
		} catch (UnknownHostException ex) {
			// ignore it
		}
		return ip + ":" + uid.toString();
	}

	/**
	 * Creates new element and appends it to the structure
	 * 
	 * @param scheme
	 * @param id
	 * @param name
	 * @param bookmark
	 * @param parentId
	 * @param created
	 * @param creater
	 * @param modified
	 * @param modifier
	 * 
	 * @return Element - newly created element
	 */
	org.opencube.oms.OMSElement createAndAppendElement(org.opencube.oms.OMSElement parentElement, String namespacePath, String schemeName, String id, String name, String bookmark, Date created,
			String creater, Date modified, String modifier) {
		org.opencube.oms.OMSElement element = (org.opencube.oms.OMSElement) this.elementsById.get(id);
		if (element == null) {
			element = new org.opencube.oms.OMSElement(this, namespacePath, schemeName, id, name, bookmark, created, creater, modified, modifier);
			this.elementsById.put(element.getId(), element);
			this.elementsByBookmark.put(element.getBookmark(), element);
			setElementParent(element, parentElement);
		}
		return element;
	}

	/**
	 * Puts the parent for the element to the map
	 * 
	 * @param element
	 * @param parent
	 */
	void setElementParent(org.opencube.oms.OMSElement element, org.opencube.oms.OMSElement parent) {
		if (parent == null) {
			parent = this.rootElement;
		}
		this.compositions.put(element, parent);
	}

	/**
	 * Returns the parent for the element
	 * 
	 * @param element -
	 *            element ot find the parent for
	 * 
	 * @return Element - the parent element
	 */
	org.opencube.oms.OMSElement getElementParent(org.opencube.oms.OMSElement element) {
		return (org.opencube.oms.OMSElement) this.compositions.get(element);
	}

	/**
	 * Removes the given element node from the structure (memory only)
	 * 
	 * @param element
	 *            oms element to remove
	 */
	public void removeElement(OMSElement element) {
		removeElement(element, true, true);
	}

	/**
	 * Completely removes the given element from the structure
	 * 
	 * @param element -
	 *            element to remove
	 * @param withChildren -
	 *            true if all subelements should be removed as well
	 */

	void removeElement(OMSElement element, boolean withChildren, boolean withRelations) {
		this.rootElement.removeRelations(element);
		OMSElement[] elements = getElements();
		for (int i = 0; elements != null && i < elements.length && withRelations; i++) {
			elements[i].removeRelations(element);
		}
		for (int i = 0; elements != null && i < elements.length && withChildren; i++) {
			if (elements[i].getParent() == element) {
				removeElement(elements[i], withChildren, withRelations);
			}
		}
		this.compositions.remove(element);
		this.elementsById.remove(element.getId());
		this.elementsByBookmark.remove(element.getBookmark());
	}

	/**
	 * 
	 * Merge this structure wit another one
	 * 
	 * @param tree -
	 *            the second (source) structure
	 * @param mode -
	 *            merging mode OMSStructure.MERGE_MODE_ADD - all elements from
	 *            source are moved to this structure, no elements are removed
	 *            OMSStructure.MERGE_MODE_REPLACE - all elements from source are
	 *            moved to this structure, but children of merged elements, not
	 *            found in source are removed from this structure
	 *            OMSStructure.MERGE_MODE_REMOVE - no elements are added to this
	 *            structure, only children of elements matched, not found in
	 *            source, are removed from this structure
	 * @throws OMSStructureException
	 */
	public void merge(OMSStructure tree, int mode) throws OMSStructureException {
		merge(tree, mode, null);
	}

	public OMSStructure cloneStructure() throws OMSStructureException {
		return this.getRootElement().replicateRecursive().getOMSStructure();
	}
	void throwComplexAttributeException(Relation r) throws OMSStructureException{ 
		throw new OMSStructureException("unable to merge unsaved strucutre. " +
				"Relation ComplexAttributeId is null." +
				"Source: " +r.getSourceElement().toString()+ " : " +r.getComplexAttribute()+" : " +r.getTargetElement().toString() 
		);
	}
	public void validateElements () throws OMSStructureException {
		OMSElement[] elements = getElements();
		for (int i=0;i<elements.length;i++)  {
			Iterator it = elements[i].relations.keySet().iterator();
			while( it.hasNext() ) {
				ArrayList list = (ArrayList)elements[i].relations.get(it.next());
				for (int j=0;j<list.size();j++) {
					Relation r = (Relation)list.get(j);
					if (r.getComplexAttributeId()==null){
						throwComplexAttributeException(r);
					}	
				}
				
			}
		}
	}
	public void merge(OMSStructure tree, int mode, ComplexAttribute[] complexAttributes) throws OMSStructureException {
		if (tree == this) {
			return;
		}
		HashSet found = new HashSet();
		OMSElement[] els = tree.getElements();

		// add new elements to this structure
		for (int i = 0; els != null && i < els.length; i++) {
			OMSElement foreignElement;

			foreignElement = els[i];

			if (!this.elementsById.containsKey(foreignElement.getId()) && mode != MERGE_MODE_REMOVE) {
				this.elementsById.put(foreignElement.getId(), foreignElement);
				this.elementsByBookmark.put(foreignElement.getBookmark(), foreignElement);
				foreignElement.setOMSStructure(this);
			}
		}

		// mark found elements, merge relations and refresh content of matched
		// ones
		for (int i = 0; els != null && i < els.length; i++) {
			OMSElement foreignElement = els[i];
			OMSElement element = (OMSElement) this.elementsById.get(foreignElement.getId());
			if (element != null) {
				found.add(element.getId());
				if (element.getParent() != null && !found.contains(element.getParent().getId())) {
					found.add(element.getParent().getId());
				}
				if (mode != MERGE_MODE_REMOVE) {

					element.merge(foreignElement, tree, mode, complexAttributes);
					element.setState(foreignElement.getState());
				}
			}
		}
		// ---------------------------------------Hier
		// Christian.................
		// fill composition performance table
		for (Iterator iter = tree.compositions.keySet().iterator(); iter.hasNext() && mode != MERGE_MODE_REMOVE;) {
			OMSElement element = (OMSElement) iter.next();
			OMSElement parent = (OMSElement) tree.compositions.get(element);
			if (!this.compositions.containsKey(element) && element.getOMSStructure() == this) {
				if (parent == tree.rootElement || !this.elementsById.containsKey(parent.getId())) {
					Relation r = parent.getCompositionRelation(element);
					if (r != null) {
						Relation newRelation = this.rootElement.createAndAppendRelation(r.getSourceElementId(), element, r.getComplexAttribute(), r.getComplexAttributeId(), true, r.getId(), r.getName(), r.isOrdered() ? r
								.getNextRelation() : null, r.getSiblingOrder(), r.getCreater(), r.getCreatingDate(), r.getModifier(), r.getModifyingDate());
						newRelation.setState(r.getState());
					}
				} else {
					OMSElement newParent = (OMSElement) this.elementsById.get(parent.getId());
					this.compositions.put(element, newParent);
				}
			}
		}
		// try to give the real parents to the elements under virtual root

		OMSElement[] rootChildren = getRootElement().getJoinedElements(OBJECTS_ATTRIBUTE);
		for (int i = 0; rootChildren != null && i < rootChildren.length; i++) {
			Relation r = getRootElement().getCompositionRelation(rootChildren[i]);
			if (r != null && r.getSourceElementId() != null) {
				OMSElement realParent = getElementById(r.getSourceElementId());
				if (realParent != null && realParent != getRootElement()) {
					int state = r.getState();
					realParent.join(r, r.getComplexAttribute(), true, rootChildren[i], r.getName(), null);
					Relation newR = realParent.getRelation(r.getComplexAttribute(), rootChildren[i]);
					newR.setState(state);
					newR.setComplexAttributeId(r.getComplexAttributeId());
				}
			}
		}

		// remove unmerged elements from merged parents
		OMSElement[] elements = getElements();
		for (int i = 0; elements != null && i < elements.length && mode != MERGE_MODE_ADD && (mode != MERGE_MODE_REFRESH || complexAttributes != null); i++) {
			OMSElement element = elements[i];
			OMSElement parent = (OMSElement) this.compositions.get(element);
			if (parent == null || (!found.contains(element.getId()) && found.contains(parent.getId()) && (complexAttributes == null || isInComplexAttributes(parent, element, complexAttributes)))) {
				removeElement(element, true, true);
			}
		}

		for (int i = 0; els != null && i < els.length; i++) {
			if (els[i].getOMSStructure() != tree) {
				tree.removeElement(els[i], true, false);
			}
		}
	}

	boolean isInComplexAttributes(OMSElement parent, OMSElement child, ComplexAttribute[] complexAttributes) throws OMSStructureException{
		for (int i = 0; i < complexAttributes.length; i++) {
			String complexAttributeName = complexAttributes[i].getName();
			Relation rel = parent.getRelation(complexAttributeName, child);
			if ( rel!=null&& rel.getComplexAttributeId()==null) { 
				 this.throwComplexAttributeException(rel);
			}
			if (rel != null && rel.getComplexAttributeId().equals(complexAttributes[i].getId())) {
				return true;
			}
		}
		return false;
	}

	public OMSStructure copy(OMSStructure tree) throws OMSStructureException {

		return this;
	}

	public void merge(OMSStructure tree) throws OMSStructureException {
		merge(tree, MERGE_MODE_ADD, null);
	}

	void resetId(String id, OMSElement element) {
		this.elementsById.remove(id);
		this.elementsById.put(element.getId(), element);
	}

	void resetBookmark(String bookmark, OMSElement element) {
		this.elementsByBookmark.remove(bookmark);
		this.elementsByBookmark.put(element.getBookmark(), element);
	}

	protected LinkedList getRelationTrash() {
		return this.relationTrash;
	}

	protected Relation[] getRemovedRelations() {
		return (Relation[]) this.relationTrash.toArray(new Relation[this.relationTrash.size()]);
	}

	protected OMSElement[] getRemovedElements() {
		return (OMSElement[]) this.elementTrash.toArray(new OMSElement[elementTrash.size()]);
	}

	void cleanRemoved() {
		OMSElement[] elements = getRemovedElements();
		for (int i = 0; i < elements.length; i++) {
			this.compositions.remove(elements[i]);
		}
		this.elementTrash.clear();
		this.relationTrash.clear();
	}

	protected LinkedList getElementTrash() {
		return elementTrash;
	}
}